package com.googlecode.gaelloon.record

import com.google.appengine.api.datastore._
import java.lang.reflect.Method
import scala.collection.mutable.ListBuffer
import scala.collection.mutable.Buffer
import scala.collection.JavaConversions.asBuffer
import java.util.ConcurrentModificationException

trait Record[RECORD_TYPE <: Record[RECORD_TYPE]] {
  this: RECORD_TYPE =>

  def meta: MetaRecord[RECORD_TYPE]

  private var was_deleted = false
  private[record] var key: Key = null

  def id: Long = if (key == null) 0 else key.getId()

  def saved = key != null

  def deleted = was_deleted

  def save(): RECORD_TYPE = meta.save(this)

  def delete(): RECORD_TYPE = {
    meta.delete(this)
    was_deleted = true
    this
  }
}

trait MetaRecord[RECORD_TYPE <: Record[RECORD_TYPE]] {
  this: RECORD_TYPE =>

  private val ds = DatastoreServiceFactory.getDatastoreService()
  private val kind: String = {
    val className = getClass.getSimpleName
    val last = className.length - 1
    val first = className.lastIndexOf('$', last - 1)
    className.substring(first + 1, last)
  }

  private var fieldList: List[FieldHolder] = {
    def rootClass = getClass.getSuperclass

    val lb = new ListBuffer[FieldHolder]
    introspectFields(this, rootClass.getMethods) {
      case (name, field, method) => {
        field.name = name
        lb += new FieldHolder(name, field, method)
      }
    }
    lb.toList
  }

  def create(): RECORD_TYPE

  def save(record: RECORD_TYPE): RECORD_TYPE = {
    val entity = if (record.key == null) new Entity(kind) else new Entity(record.key)
    fieldList.foreach {fh => entity.setProperty(fh.name, fh(record).dsValue)}

    ds.put(entity)

    record.key = entity.getKey
    record
  }

  def get(id: Long): RECORD_TYPE = {
    val key = KeyFactory.createKey(kind, id)
    val entity = try {
      ds.get(key)
    } catch {
      case e: EntityNotFoundException => throw new RecordNotFoundException(id)
    }

    entity2Record(entity)
  }

  private def entity2Record(entity: Entity) = {
    val record = create()

    record.key = entity.getKey
    fieldList.foreach {fh => fh(record).dsValue = entity.getProperty(fh.name)}

    record
  }

  def delete(record: RECORD_TYPE): RECORD_TYPE = {
    val e = new Entity(kind)
    ds.delete(record.key)
    record.key = null
    record
  }

  def find(params: QueryParam*): Option[RECORD_TYPE] = {
    val pq = prepareQuery(params: _*)._1

    val entity = try {
      pq.asSingleEntity()
    } catch {
      case e: PreparedQuery.TooManyResultsException => throw new TooManyRowsException
    }
    if (entity == null) None else Some(entity2Record(entity))
  }

  def findAll(params: QueryParam*): List[RECORD_TYPE] = {
    val (pq, fetchOptions) = prepareQuery(params: _*)

    val records = new ListBuffer[RECORD_TYPE]
    val res: Buffer[Entity] = pq.asList(fetchOptions)
    res.foreach {records += entity2Record(_)}
    records.toList
  }

  def count(params: QueryParam*): Int = {
    val pq = prepareQuery(params: _*)._1

    pq.countEntities()
  }

  def inTransaction[T](op: => T): T = inTransaction(10)(op)

  def inTransaction[T](retries: Int)(op: => T): T = {
    require(retries >= 0)

    val txn = ds.beginTransaction()
    try {
      val res = op
      txn.commit()
      res
    } catch {
      case e: ConcurrentModificationException => if (retries == 0) throw e else {
        if (txn.isActive) txn.rollback()
        inTransaction(retries - 1)(op)
      }
    } finally {
      if (txn.isActive) txn.rollback()
    }
  }

  private def prepareQuery(params: QueryParam*) = {
    val query = new Query(kind)
    var fetchOptions = FetchOptions.Builder.withDefaults
    params.foreach {
      _ match {
        case Is(field, value) => query.addFilter(field.name, Query.FilterOperator.EQUAL, value)
        case Not(field, value) => query.addFilter(field.name, Query.FilterOperator.NOT_EQUAL, value)
        case LessThen(field, value) => query.addFilter(field.name, Query.FilterOperator.LESS_THAN, value)
        case LessThenOrIs(field, value) => query.addFilter(field.name, Query.FilterOperator.LESS_THAN_OR_EQUAL, value)
        case GreaterThen(field, value) => query.addFilter(field.name, Query.FilterOperator.GREATER_THAN, value)
        case GreaterThenOrIs(field, value) => query.addFilter(field.name, Query.FilterOperator.GREATER_THAN_OR_EQUAL, value)
        case In(field, value) => query.addFilter(field.name, Query.FilterOperator.IN, java.util.Arrays.asList(value.toArray: _*))
        case Asc(field) => query.addSort(field.name, Query.SortDirection.ASCENDING)
        case Desc(field) => query.addSort(field.name, Query.SortDirection.DESCENDING)
        case Limit(limit) => fetchOptions = fetchOptions.limit(limit)
        case Offset(offset) => fetchOptions = fetchOptions.offset(offset)
      }
    }

    (ds.prepare(ds.getCurrentTransaction(null), query), fetchOptions)
  }

  private def introspectFields(rec: RECORD_TYPE, methods: Array[Method])(f: (String, Field[Any, RECORD_TYPE], Method) => Any) = {
    def isField(m: Method) = classOf[Field[_, _]].isAssignableFrom(m.getReturnType)

    for (method <- methods if isField(method)) {
      method.invoke(rec) match {
        case field: Field[_, _] =>
          f(method.getName, field.asInstanceOf[Field[Any, RECORD_TYPE]], method)
        case _ =>
      }
    }
  }

  private class FieldHolder(val name: String, private val field: Field[Any, RECORD_TYPE], private val method: Method) {
    def apply(record: RECORD_TYPE): Field[Any, RECORD_TYPE] = method.invoke(record).asInstanceOf[Field[Any, RECORD_TYPE]]
  }
}

trait Field[FIELD_TYPE, RECORD_TYPE <: Record[RECORD_TYPE]] {
  private[record] var name: String = null
  private var valueIntr: Option[FIELD_TYPE] = None

  def default: FIELD_TYPE

  private[record] def dsValue: Any = {
    val res = valueIntr match {
      case Some(v) => v
      case _ => default
    }

    if (res == null) null else toDs(res)
  }

  def is(v: FIELD_TYPE): QueryParam = Is(this, v)

  def not(v: FIELD_TYPE): QueryParam = Not(this, v)

  def lessThen(v: FIELD_TYPE): QueryParam = LessThen(this, v)

  def lessThenOrIs(v: FIELD_TYPE): QueryParam = LessThenOrIs(this, v)

  def greaterThen(v: FIELD_TYPE): QueryParam = GreaterThen(this, v)

  def greaterThenOrIs(v: FIELD_TYPE): QueryParam = GreaterThenOrIs(this, v)

  def in(v: List[FIELD_TYPE]): QueryParam = In(this, v)

  def asc(): QueryParam = Asc(this)

  def desc(): QueryParam = Desc(this)

  private[record] def dsValue_=(v: Any) {
    valueIntr = if (v == null) None else Some(fromDs(v))
  }

  protected def fromDs(v: Any): FIELD_TYPE

  protected def toDs(v: FIELD_TYPE): Any

  def set(v: FIELD_TYPE) {
    valueIntr = if (v == null) None else Some(v)
  }

  def value: FIELD_TYPE = valueIntr match {
    case Some(v) => v
    case _ => default
  }

  override def toString = value match {
    case Some(v) => v.toString
    case _ => "null"
  }
}

class ByteField[RECORD_TYPE <: Record[RECORD_TYPE]] extends Field[Byte, RECORD_TYPE] {
  def default: Byte = 0

  def fromDs(v: Any): Byte = v.asInstanceOf[Long].toByte

  def toDs(v: Byte): Any = v.toLong
}

class ShortField[RECORD_TYPE <: Record[RECORD_TYPE]] extends Field[Short, RECORD_TYPE] {
  def default: Short = 0

  def fromDs(v: Any): Short = v.asInstanceOf[Long].toShort

  def toDs(v: Short): Any = v.toLong
}

class IntField[RECORD_TYPE <: Record[RECORD_TYPE]] extends Field[Int, RECORD_TYPE] {
  def default: Int = 0

  def fromDs(v: Any): Int = v.asInstanceOf[Long].toInt

  def toDs(v: Int): Any = v.toLong
}

class LongField[RECORD_TYPE <: Record[RECORD_TYPE]] extends Field[Long, RECORD_TYPE] {
  def default: Long = 0L

  def fromDs(v: Any): Long = v.asInstanceOf[Long]

  def toDs(v: Long): Any = v
}

class FloatField[RECORD_TYPE <: Record[RECORD_TYPE]] extends Field[Float, RECORD_TYPE] {
  def default: Float = 0.0F

  def fromDs(v: Any): Float = v.asInstanceOf[Double].toFloat

  def toDs(v: Float): Any = v.toDouble
}

class DoubleField[RECORD_TYPE <: Record[RECORD_TYPE]] extends Field[Double, RECORD_TYPE] {
  def default: Double = 0.0

  def fromDs(v: Any): Double = v.asInstanceOf[Double]

  def toDs(v: Double): Any = v
}

class CharField[RECORD_TYPE <: Record[RECORD_TYPE]] extends Field[Char, RECORD_TYPE] {
  def default: Char = 0

  def fromDs(v: Any): Char = v.asInstanceOf[Long].toChar

  def toDs(v: Char): Any = v.toLong
}

class StringField[RECORD_TYPE <: Record[RECORD_TYPE]] extends Field[String, RECORD_TYPE] {
  def default: String = null

  def fromDs(v: Any): String = v match {
    case str: String => str
    case txt: Text => txt.getValue
    case _ => v.asInstanceOf[ScalaObject].toString
  }

  def toDs(v: String): Any =
    if (v.length <= DataTypeUtils.MAX_STRING_PROPERTY_LENGTH) v else new Text(v)
}

class RecordNotFoundException(id: Long) extends Exception(id.toString)
class TooManyRowsException extends Exception

sealed trait QueryParam
case class Is[FIELD_TYPE, RECORD_TYPE <: Record[RECORD_TYPE]](field: Field[FIELD_TYPE, RECORD_TYPE], value: FIELD_TYPE) extends QueryParam
case class Not[FIELD_TYPE, RECORD_TYPE <: Record[RECORD_TYPE]](field: Field[FIELD_TYPE, RECORD_TYPE], value: FIELD_TYPE) extends QueryParam
case class LessThen[FIELD_TYPE, RECORD_TYPE <: Record[RECORD_TYPE]](field: Field[FIELD_TYPE, RECORD_TYPE], value: FIELD_TYPE) extends QueryParam
case class LessThenOrIs[FIELD_TYPE, RECORD_TYPE <: Record[RECORD_TYPE]](field: Field[FIELD_TYPE, RECORD_TYPE], value: FIELD_TYPE) extends QueryParam
case class GreaterThen[FIELD_TYPE, RECORD_TYPE <: Record[RECORD_TYPE]](field: Field[FIELD_TYPE, RECORD_TYPE], value: FIELD_TYPE) extends QueryParam
case class GreaterThenOrIs[FIELD_TYPE, RECORD_TYPE <: Record[RECORD_TYPE]](field: Field[FIELD_TYPE, RECORD_TYPE], value: FIELD_TYPE) extends QueryParam
case class In[FIELD_TYPE, RECORD_TYPE <: Record[RECORD_TYPE]](field: Field[FIELD_TYPE, RECORD_TYPE], value: List[FIELD_TYPE]) extends QueryParam
case class Limit(limit: Int) extends QueryParam
case class Offset(offset: Int) extends QueryParam
case class Asc[FIELD_TYPE, RECORD_TYPE <: Record[RECORD_TYPE]](field: Field[FIELD_TYPE, RECORD_TYPE]) extends QueryParam
case class Desc[FIELD_TYPE, RECORD_TYPE <: Record[RECORD_TYPE]](field: Field[FIELD_TYPE, RECORD_TYPE]) extends QueryParam
